﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;

namespace theRift
{
    // Triangle, id to recognize with, 3 points (duh), and a normal 
    public class Triangle
    {
        public int id;
        public Vector3 point1;
        public Vector3 point2;
        public Vector3 point3;
        public Vector3 normal;
    }

    public class Terrain : DrawableGameComponent
    {
        protected VertexDeclaration vertexDeclaration;
        protected VertexBuffer vertexBuffer;
        protected IndexBuffer indexBuffer;
        protected VertexPositionNormalTexture[] vertices;
        protected int[] indices;
        protected Triangle[] triangles;

        protected Color[] bits;
        protected Texture2D heightmap;
        protected string heightmapFile;

        protected Transformation transformation;
        protected Vector2 tileScale = new Vector2(24.0f, 24.0f);
        protected float minPoint = 0;
        protected float maxPoint = 0;
        protected float maxHeight = 80.0f;

        protected Point size; // Actual number of tiles
        protected float width; // size * tileScale
        protected float depth; // size * tileScale
        protected Vector3 center; // Center of our heightmap

        // Shaders
        protected BasicEffect effect;

        protected Texture2D grassTexture;

        // Essential
        protected LightManager lightManager;
        protected CameraManager cameraManager;
        protected bool isInitialized;

        public Triangle[] Triangles
        {
            get { return triangles; }
            set { triangles = value; }
        }

        public Vector2 TileScale
        {
            get { return tileScale; }
        }

        public Terrain(Game game, Vector2 tileScale)
            : base(game)
        {
            transformation = new Transformation();
            this.tileScale = tileScale;
            this.isInitialized = false;

            grassTexture = Game.Content.Load<Texture2D>(Directories.TEXTURES_PATH + "grass");
        }

        // Generate a new height map, very basic for now but creates a blank heightmap
        // so it's a start. Needs options for predefined heightmap sizes and an option
        // to generate random terrain fluctuations.
        public void GenerateHeightMap()
        {
            if (!isInitialized)
                Initialize();

            size = new Point(32, 32); // Small for testing
            
            // Use the given size to calculate width, height
            // and center of our heightmap.
            width = size.X * tileScale.X;
            depth = size.Y * tileScale.Y;
            center = new Vector3(width * 0.5f, 0, depth * 0.5f);

            // Create an "empty" terrain
            bits = new Color[size.X * size.Y];
            heightmap = new Texture2D(Game.GraphicsDevice, 
                size.X, size.Y, 1, TextureUsage.None, SurfaceFormat.Color);
            heightmap.SetData<Color>(bits);

            // Terrain data set, let's initialize and for one transform it
            // into a mesh!
            InitTerrain();
        }

        // Place to load in an already existing heightmap's data and prepare for
        // initialization into a terrain mesh.
        public void LoadHeightData(string filename)
        {
            if (!isInitialized)
                Initialize();

            // Load in our heightmap and grab it's size for future usage.
            heightmap = Game.Content.Load<Texture2D>(Directories.TERRAINS_PATH + filename);
            size = new Point(heightmap.Width, heightmap.Height);

            // Use the given size to calculate width, height
            // and center of our heightmap.
            width = size.X * tileScale.X;
            depth = size.Y * tileScale.Y;
            center = new Vector3(width * 0.5f, 0, depth * 0.5f);

            // Store our heightmap data
            bits = new Color[size.X * size.Y];
            heightmap.GetData<Color>(bits);

            // Terrain data set, let's initialize and for one transform it
            // into a mesh!
            InitTerrain();
        }

        public override void Initialize()
        {
            cameraManager = Game.Services.GetService(typeof(CameraManager)) as CameraManager;
            lightManager = Game.Services.GetService(typeof(LightManager)) as LightManager;

            if (cameraManager == null)
                throw new InvalidOperationException(Error.NO_CAMERAMANAGER);

            if (lightManager == null)
                throw new InvalidOperationException(Error.NO_LIGHTMANAGER);

            isInitialized = true;
            
            base.Initialize();
        }

        public void InitTerrain()
        {
            effect = new BasicEffect(Game.GraphicsDevice, null);
            SetEffectParameters();

            GenerateMesh();

            // Centralize the terrain
            transformation.Translate = -center;
        }

        public void GenerateMesh()
        {
            GenerateVertices();
            GenerateIndices();
            GenerateVertexNormals();
            SetVertexBuffer();
            SetIndexBuffer();
        }

        private void GenerateVertices()
        {
            // Ensure we have our vertices ready to work with and
            // that the number is valid. If not create our vertices
            // by size of our heightmap.
            if (vertices == null || vertices.Length != bits.Length)
                vertices = new VertexPositionNormalTexture[bits.Length];

            // Vertex position will first be stored in this container.
            // minPoint is set to maxHeight so pointHeight can actually
            // be smaller than minPoint.
            Vector3 vertexPos = new Vector3();
            maxPoint = 0;
            minPoint = maxHeight;

            for (int z = 0; z < size.Y; z++)
            {
                for (int x = 0; x < size.X; x++)
                {
                    // Grab the height of the current point in our heightmap
                    float pointHeight = (float)bits[x + z * size.X].ToVector4().X;
                    vertexPos.X = x * tileScale.X;
                    vertexPos.Z = z * tileScale.Y;
                    vertexPos.Y = pointHeight * maxHeight;
                    
                    vertices[x + z * size.X].Position = vertexPos;
                    
                    // Scalable texture coordinates, not that great {pretty experimental}
                    vertices[x + z * size.X].TextureCoordinate = new Vector2(
                        x / ((float)size.X / (float)Math.Pow((tileScale.X + 1), (3.5 - tileScale.X))),
                        z / ((float)size.Y / (float)Math.Pow((tileScale.Y + 1), (3.5 - tileScale.Y))));
                    vertices[x + z * size.X].Normal = Vector3.Up;

                    // Found a new maxPoint or new minPoint?
                    if (pointHeight > maxPoint)
                        maxPoint = pointHeight;
                    if (pointHeight < minPoint)
                        minPoint = pointHeight;
                }
            }
        }

        private void GenerateVertexNormals()
        {
            // Set our normals to 0
            for (int i = 0; i < vertices.Length; i++)
                vertices[i].Normal = new Vector3(0, 0, 0);

            bool swappedWinding = false;
            for (int i = 2; i < indices.Length; i++)
            {
                // Create our normals
                Vector3 v1 = vertices[indices[i - 1]].Position - vertices[indices[i]].Position;
                Vector3 v2 = vertices[indices[i - 2]].Position - vertices[indices[i]].Position;
                Vector3 normal = Vector3.Cross(v1, v2);
                normal.Normalize();

                // Winding swapped or not? If so, multiply normal by -1
                // to get a desired effect.
                if (swappedWinding)
                    normal *= -1;

                if (!float.IsNaN(normal.X))
                {
                    vertices[indices[i]].Normal += normal;
                    vertices[indices[i - 1]].Normal += normal;
                    vertices[indices[i - 2]].Normal += normal;
                }

                swappedWinding = !swappedWinding;
            }

            // Normalize our normals
            for (int i = 0; i < vertices.Length; i++)
                vertices[i].Normal.Normalize();
        }

        private void SetVertexBuffer()
        {
            // Set our vertex declaration and buffer
            vertexDeclaration = new VertexDeclaration(Game.GraphicsDevice,
                VertexPositionNormalTexture.VertexElements);

            vertexBuffer = new VertexBuffer(Game.GraphicsDevice,
                typeof(VertexPositionNormalTexture), bits.Length,
                BufferUsage.None);
            vertexBuffer.SetData<VertexPositionNormalTexture>(vertices);
        }

        private void GenerateIndices()
        {
            triangles = new Triangle[(size.X - 1) * (size.Y - 1) * 2];
            indices = new int[3 * triangles.Length]; // 3 * number of triangles

            int currentIndice = 0;
            for (int z = 0; z < size.Y - 1; z++)
            {
                for (int x = 0; x < size.X - 1; x++)
                {
                    // First triangle                    
                    indices[currentIndice++] = x + (z + 1) * size.X; // top left
                    indices[currentIndice++] = (x + 1) + z * size.X; // lower right
                    indices[currentIndice++] = x + z * size.X; // lower left

                    // Second triangle                    
                    indices[currentIndice++] = x + (z + 1) * size.X; // top left
                    indices[currentIndice++] = (x + 1) + (z + 1) * size.X; // top right
                    indices[currentIndice++] = (x + 1) + z * size.X; // lower right

                    GenerateCollisionData(currentIndice, (currentIndice / 3) - 2, x, z);
                }
            }
        }

        // Gather the vertex positions for our triangles and store it within our
        // triangle attribute, this can be used for collision detection on the terrain
        // by other classes.
        private void GenerateCollisionData(int indice, int triangleID, int x, int z)
        {
            triangles[triangleID] = new Triangle();
            triangles[triangleID].point1 = vertices[x + z * size.X].Position;
            triangles[triangleID].point2 = vertices[x + (z + 1) * size.X].Position;
            triangles[triangleID].point3 = vertices[(x + 1) + z * size.X].Position;
            triangles[triangleID].normal = GetNormal(triangles[triangleID].point1,
                triangles[triangleID].point2, triangles[triangleID].point3);
            triangles[triangleID].id = indice / 6 - 1;

            triangles[triangleID + 1] = new Triangle();
            triangles[triangleID + 1].point1 = vertices[(x + 1) + z * size.X].Position;
            triangles[triangleID + 1].point2 = vertices[x + (z + 1) * size.X].Position;
            triangles[triangleID + 1].point3 = vertices[(x + 1) + (z + 1) * size.X].Position;
            triangles[triangleID + 1].normal = GetNormal(triangles[triangleID + 1].point1,
                triangles[triangleID + 1].point2, triangles[triangleID + 1].point3);
            triangles[triangleID + 1].id = indice / 6;
        }

        public static Vector3 GetNormal(Vector3 p1, Vector3 p2, Vector3 p3)
        {
            // Calculate vectors v1->v2 and v3->v1 and normal as cross product
            Vector3 vu = p2 - p1;
            Vector3 vt = p1 - p3;
            Vector3 normal = Vector3.Cross(vu, vt);
            normal.Normalize();

            return normal;
        }

        private void SetIndexBuffer()
        {
            indexBuffer = new IndexBuffer(Game.GraphicsDevice, sizeof(int) * indices.Length,
                BufferUsage.WriteOnly, IndexElementSize.ThirtyTwoBits);
            indexBuffer.SetData<int>(indices);
        }

        private void SetEffectParameters()
        {
            effect.Texture = grassTexture;
            effect.TextureEnabled = true;

            effect.EnableDefaultLighting();
            Vector3 lightDirection = new Vector3(1, -1, 1);
            lightDirection.Normalize();
            effect.DirectionalLight0.Direction = lightDirection;
            effect.DirectionalLight0.Enabled = true;
            effect.AmbientLightColor = lightManager.AmbientLightColor;
            effect.DirectionalLight1.Enabled = false;
            effect.DirectionalLight2.Enabled = false;
            effect.SpecularColor = new Vector3(0, 0, 0);
        }

        // Get position on terrain based on a 2D pointer position (Mouse input 
        // or possibly other input device) and translate it into a 3D position
        // on the terrain.
        public void GetPosition(Vector2 pointerScreenPos, out Vector3 pointerPos)
        {
            Ray pointerRay;
            pointerPos = Vector3.Zero;

            RayHelper.GetPointerRay(pointerScreenPos, cameraManager, out pointerRay);
            float rayLength = 0f;

            for (int i = 0; i < Triangles.Length; i++)
            {
                Triangle triangle = Triangles[i];

                if (RayHelper.Intersects(pointerRay, triangle.point1, triangle.point3, triangle.point2, triangle.normal, false, true, out rayLength))
                {
                    Vector3 rayTarget = pointerRay.Position + pointerRay.Direction * rayLength;
                    pointerPos = new Vector3((rayTarget.X / TileScale.X), 0, (rayTarget.Z / TileScale.Y));
                    pointerPos.Y = GetHeight(pointerPos.X, pointerPos.Y);
                }
            }

            Game.IsMouseVisible = true;
        }

        //
        public float GetHeight(float x, float z)
        {
            bool invalid = x < 0;
            invalid |= z < 0;
            invalid |= x >= size.X - 1;
            invalid |= z >= size.Y - 1;

            if (invalid)
                return 10;

            int xLower = (int)x;
            int xHigher = xLower + 1;
            float xRelative = (x - xLower) / ((float)xHigher - (float)xLower);

            int zLower = (int)z;
            int zHigher = zLower + 1;
            float zRelative = (z - zLower) / ((float)zHigher - (float)zLower);

            float heightLxLz = vertices[xLower + zLower * size.X].Position.Y;
            float heightLxHz = vertices[xLower + zHigher * size.X].Position.Y;
            float heightHxLz = vertices[xHigher + zLower * size.X].Position.Y;
            float heightHxHz = vertices[xHigher + zHigher * size.X].Position.Y;

            bool cameraAboveLowerTriangle = (xRelative + zRelative < 1);

            float finalHeight;
            if (cameraAboveLowerTriangle)
            {
                finalHeight = heightLxLz;
                finalHeight += zRelative * (heightLxHz - heightLxLz);
                finalHeight += xRelative * (heightHxLz - heightLxLz);
            }
            else
            {
                finalHeight = heightHxHz;
                finalHeight += (1.0f - zRelative) * (heightHxLz - heightHxHz);
                finalHeight += (1.0f - xRelative) * (heightLxHz - heightHxHz);
            }

            return finalHeight;
        }

        public override void Draw(GameTime gameTime)
        {
            // Can't do this without a camera
            if (cameraManager.ActiveCamera == null)
                throw new Exception("An active camera is required in order to draw the terrain!");

            // Set effect values
            effect.World = Matrix.Identity; //transformation.Matrix;
            effect.View = cameraManager.ActiveCamera.View;
            effect.Projection = cameraManager.ActiveCamera.Projection;

            // Clockwise cullmode needed for terrain
            Game.GraphicsDevice.RenderState.CullMode = CullMode.CullClockwiseFace;

            // Wireframe on for now, terrain has no textures
            // so this makes it a little easier to see coding effects
            //Game.GraphicsDevice.RenderState.FillMode = FillMode.WireFrame;

            // Begin effect
            effect.Begin();

            // For each pass...
            foreach (EffectPass pass in effect.CurrentTechnique.Passes)
            {
                // Begin the pass
                pass.Begin();

                // Draw terrain vertices and indices
                Game.GraphicsDevice.Vertices[0].SetSource(vertexBuffer, 0, VertexPositionNormalTexture.SizeInBytes);
                Game.GraphicsDevice.Indices = indexBuffer;
                Game.GraphicsDevice.VertexDeclaration = vertexDeclaration;
                Game.GraphicsDevice.DrawUserIndexedPrimitives(PrimitiveType.TriangleList, vertices, 0, 
                    vertices.Length, indices, 0, triangles.Length);

                // End the pass
                pass.End();
            }

            // End effect
            effect.End();

            // Reset to counterclockwise cullmode!
            Game.GraphicsDevice.RenderState.CullMode = CullMode.CullCounterClockwiseFace;

            // Reset fillmode to solid!
            //Game.GraphicsDevice.RenderState.FillMode = FillMode.Solid;

            base.Draw(gameTime);
        }
    }
}